Skip to content

docs(agents): correct input capability list in AGENTS.md - #588

Merged
stormmuller merged 2 commits into
devfrom
claude/github-issue-582-okfmsl
Aug 7, 2026
Merged

docs(agents): correct input capability list in AGENTS.md#588
stormmuller merged 2 commits into
devfrom
claude/github-issue-582-okfmsl

Conversation

@stormmuller

Copy link
Copy Markdown
Member

Summary

AGENTS.md's Project Overview listed the engine's input capabilities as "Keyboard, mouse, and touch input handling". src/input/ has keyboard, mouse, and gamepad input sources — there is no TouchInputSource. Corrected the line to say "Keyboard, mouse, and gamepad input handling".

While in there, audited the rest of the Project Overview list against /src: ECS, rendering (WebGL2), physics (rigid bodies, collision, gravity), audio (Howler.js), animations, particles, asset loading, and FSM all match their implementations — no further drift found. Also checked README.md and documentation-site/docs for the same "touch" claim; neither duplicates it, so no other file needed a change.

This is documentation-only; no TouchInputSource was added (that's explicitly out of scope per the issue).

Related issue(s)

Closes #582

Verification checklist

  • npm run check-types passes with 0 errors (no /src changes made)
  • npm test passes (no /src changes made)
  • npm run lint passes with 0 errors
  • npm run cspell passes with 0 errors
  • npm run check-exports passes (no /src changes made)
  • Any new/changed public API is exported from the module's index.ts — N/A, no API change
  • Documentation under /documentation-site/docs/docs is updated if this change affects documented behavior — N/A, no documented behavior changed, AGENTS.md itself is the doc being corrected
  • If this change touches a module with a demo — N/A, no /src module touched

Changelog

  • Not required — Conventional Commits type is docs

Generated by Claude Code

src/input/ has keyboard, mouse, and gamepad input sources, not touch.
Also audited the rest of the Project Overview list against /src; the
other bullets (ECS, rendering, physics, audio, animations, particles,
asset loading, FSM) already match the implementation, and no other
file duplicates the touch claim.

Fixes #582
@stormmuller
stormmuller enabled auto-merge (squash) August 6, 2026 21:59
The Aug 7 2026 GitHub Actions incident (ARC runner pods stuck idle)
dropped this branch's push/pull_request-triggering events, so the
required CI workflow never ran on this PR. Empty commit to retrigger it.
@stormmuller
stormmuller merged commit 97c8ca7 into dev Aug 7, 2026
12 checks passed
@stormmuller
stormmuller deleted the claude/github-issue-582-okfmsl branch August 7, 2026 07:25
@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

stormmuller pushed a commit that referenced this pull request Aug 7, 2026
…ng on dev

Merges dev, which brings in #587 (the parent-transform fix filed as #581) and
#588 (the AGENTS.md input capability correction filed as #582). Both were
surfaced by the review of this document, and both are now fixed, so the
document's claims about them were stale.

The transform gap row and its explanatory note move to past tense. #587 landed
exactly the composition the note proposed - scale the child's local offset by
the parent's world scale, rotate it by the parent's world rotation, then add -
and deleted the three superseded parent-*-system.ts files along with it. The
note is retained rather than removed, since the design's assumptions were formed
against the broken behavior and it is worth knowing they no longer are.

DL-03's "does this survive the fix" subsection moves to past tense too, and now
records that option (b) is no longer broken while still losing on the
load-bearing arguments.

Open question 2 loses its blocker: world-space canvas mode depended on the
transform fix, since a health bar parented to a rotating ship was exactly the
broken case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw
stormmuller added a commit that referenced this pull request Aug 8, 2026
* docs(ui): add design document for a uGUI-style UI system

Adds /design/ui-system.md, a full architecture and delivery plan for a
retained-mode, ECS-native UI system modelled on Unity's uGUI (Canvas /
RectTransform / Graphic / EventSystem) rather than IMGUI or UI Toolkit.

Covers coordinate spaces, the per-frame system pipeline and its ordering
constraints, RectTransform anchor/pivot resolution, the text rendering
pipeline, and the pointer interaction state machine, plus a twelve-entry
decision log, a six-phase feature backlog, risks, and a testing strategy.

Also documents the new /design directory in AGENTS.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): name the model generically and correct the draw-order claim

Addresses two review comments on the UI design document.

Replaces "uGUI" as the name for Forge's chosen model with the descriptive
"anchored rect tree", used consistently throughout. Unity, Godot, and Flash
are still cited, but only as prior art in the comparison table and where the
text refers specifically to Unity's implementation - Forge no longer borrows
another engine's product name as its own vocabulary. The immediate-mode and
markup-and-stylesheet alternatives are likewise named by what they are rather
than by Unity's product names for them.

Corrects the "hierarchy order is draw order" bullet, which overclaimed. Draw
order in Forge is a chain - culling mask, camera layer and query order, sprite
layer, then depth from world Y - so hierarchy ordering is a property this
design must build (via DL-06), not one it inherits, and it holds only within a
single canvas and sprite layer. Spells that chain out explicitly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): correct the parent-transform claim and link it to issue #581

The §4.1 table said entity hierarchy "ignores parent rotation/scale", which
reads as though neither propagates. That is wrong: composeWithParent inherits
rotation additively and scale multiplicatively, both correctly. What is
actually broken is narrower - a child's local position offset is composed by
plain addition, so it is never rotated or scaled by the parent's world
transform, and a child of a rotating parent spins in place instead of
orbiting.

Restates the gap precisely, adds a note working through the missing terms, a
concrete reproduction, and the finding that transform-system.test.ts never
exercises a rotated or scaled parent so nothing locks the behaviour in. Also
records that the three superseded parent-*-system.ts files carry the same
composition but are unexported and referenced only by their own tests.

Filed as #581. The design no longer treats this as a constraint to work
around; DL-03's rationale is restated so it no longer leans on the bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): drop touch input from scope and link the AGENTS.md accuracy issue

Touch input is out of scope for the UI design. Removes backlog item 0.2
(TouchInputSource), leaving the numbering gap in place so the decision stays
visible rather than being silently renumbered.

DL-07 now states the consequence explicitly: PointerStateEcsComponent is
specified as a source-agnostic pointer written by MouseInputSource today, so a
TouchInputSource can later become another writer of the same component without
any system above it changing. The stale getBoundingClientRect fix remains a
genuine prerequisite and is called out as the only one.

The AGENTS.md claim of touch support, which does not exist in /src/input, is
tracked in #582 rather than being fixed as a side effect of this design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): switch draw order to sprite.sortDepth, fix the gap table, scope out clipping

Three review points.

DL-06 reversed. Draw order now comes from an optional sortDepth on
SpriteEcsComponent rather than the dead DepthEcsComponent. layer already lives
on the sprite and its JSDoc already documents the world-Y tie-break that
sortDepth overrides, so splitting one sort key across two components was
arbitrary. It is also free: buildCameraCommands runs per camera and already
does three getComponent calls per sprite, and this design adds a second camera
- a separate component made that four times sprites times cameras, where a
sprite field is a property read on an object already in the batch. A separate
component also buys no composition, since a draw-order key is meaningless on an
entity with nothing to draw. DepthEcsComponent is now slated for deletion
(item 0.11) rather than resurrection.

Gap table rebuilt. The severity column mixed urgency, category, and scope
("Blocking", "Opportunity", "Expected", "Bug"), so "Expected" answered nothing.
Replaced with one ordered priority scale plus a column naming the backlog item
or issue that owns each gap. The rect/rect-transform gap is a Blocker, not a
shrug - it is foundational and must be built first.

Clipping and masking moved out of scope, tracked in #583. Like text, it is
generally useful outside UI - minimaps, wipe transitions, fog-of-war,
fill-by-reveal bars - so it belongs in /src/rendering. DL-09 keeps the decision
record since the UI design surfaced it, and notes it blocks only ScrollRect
(3.4), not Phases 0-2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): move text rendering out of scope, tracked in #584

Text is generally useful outside the UI module - damage numbers, dialogue,
floating names, debug overlays - so like clipping it belongs in its own module
rather than buried under /src/ui. Filed as #584 covering MSDF atlas loading,
the shader, shaping, and a default shipped atlas.

The sub-quad expansion refactor of render-system.ts moves with it. It existed
to serve text, and it was the highest-risk item in this plan, so the UI
critical path no longer carries it. DL-04 and DL-05 stay as decision records
with scope banners pointing at #584.

Phase 0 is rebuilt and renumbered: what remains is the canvas-space pointer,
Rect2, sprite.sortDepth, deleting DepthEcsComponent, and verifying UI-camera
compositing. A table records what moved out and where, since three separate
removals had left the numbering full of holes.

States the dependency plainly rather than hiding it: every phase can be built
and unit-tested without text, but the module is not useful without it, so text
landing should gate the UI module's first release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): move interaction events onto the interactable, drop ButtonEcsComponent

Adds DL-13, covering two related review points.

Buttons are not the only clickable things. Toggles, sliders, scrollbar thumbs,
list rows, inventory slots, cards and close icons all want onClick and the
pointer enter/exit pair, so hanging the event surface off ButtonEcsComponent
forced each of them to claim to be a button or duplicate the events. Events now
live on UiInteractableEcsComponent - the component that already means "this
rect participates in pointer input".

That leaves nothing for a button component to hold: visual feedback belongs to
a transition component and focus to UiFocusEcsComponent. A button is fully
described by interactable + transitions + a child label, so it becomes
createButton, an aggregate factory following the existing createCamera
precedent.

UiPointerStateEcsComponent is merged in as well. The State suffix was the tell
- every component is state, and nothing ever wanted the interaction state
without the interactable. The codebase already keeps author-set and
system-written fields together where they belong: PositionEcsComponent holds
local beside world. Input's pointer component is renamed PointerEcsComponent
for the same reason.

Also records the sprite pivot Y-axis inconsistency as issue #585 rather than
documenting it as a permanent gotcha; once fixed, the compensating
`1 - pivot.y` bridge in the layout system should be deleted, not kept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): specify same-tick pointer transitions so Hovered can be skipped

The interaction state machine had no path from Normal to Pressed, which
implied a press could not be observed on the same tick the pointer entered.
That is wrong twice over.

Input is sampled per tick, not streamed: MouseInputSource accumulates button
downs and ups into per-frame sets and resets them each tick, so a system sees
the current position plus a set of edges, never their intra-frame ordering. A
flick-and-click fits inside one 16.7ms frame, and touch has no hover phase at
all - its first event is simultaneously entered and down. Since DL-07 specifies
the pointer source-agnostically so touch can be added later without revisiting
the design, the state machine has to tolerate a missing hover for that promise
to hold.

The related case is press and release within one tick, from a synthetic or
fast enough click. Both edges survive the frame in separate sets, but a system
reading only "is the button held" sees neither and drops the click.

Specifies the system as deriving state from each tick's facts and events from
the delta against the previous tick, rather than applying one transition per
tick, and adds the Normal to Pressed edge so that path is legal rather than
accidental. Callers are told to treat the states as sampled, not as a
guaranteed sequence. Both same-tick cases are added to the unit test plan,
since they are cheap to assert synthetically and nearly impossible to
reproduce by hand.

Also fixes a duplicate const in the polled/evented example.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): make activation source-agnostic via InputAction, not a pointer click

Adds DL-14. The interaction model was pointer-centric - onClick, raised only by
a mouse - with gamepad navigation parked in Phase 5 polish. A controller has no
cursor, no hover and no click; it has a focused element and a submit action, so
that shape either excluded controllers or bolted them on as a parallel path
every consumer handles twice.

The engine already solved this a layer down and this design should not
re-solve it differently. /src/input decouples InputAction from InputSource so
game code says "jump was actioned" rather than "space was pressed", and
CameraEcsComponent already consumes that directly via zoomInput/panInput
rather than reading keys. The UI canvas now takes
submitInput/cancelInput/navigateInput the same way and never touches an input
source.

onClick becomes onActivate, raised from either the pointer path or the focus
path, with the source never exposed. isHovered stays pointer-only; isFocused is
source-agnostic; transitions key off a derived state merging both so an element
highlights the same whether moused-over or stick-focused. Drag events stay
explicitly pointer-shaped, since a drag has no controller analogue.
UiFocusEcsComponent and the navigation system move from Phase 5 to Phase 2 core.

This also makes the UI programmatically drivable for free: because activation
arrives through an InputAction, a test or scripted tutorial calls
action.trigger() and the UI responds exactly as it would to a player - no
synthesized DOM events and no test-only code path.

Separately, addresses the DX concern that an author should not assemble cameras
and canvases to show a button. Adds a "minimal path" showing the two calls that
put a working, gamepad-navigable button on screen, and states that every option
is defaulted and the second camera is an implementation detail of
createUiCanvas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): document post-processing scope as a choice, not a constraint

DL-01 presented a separate UI render target as giving "the right
post-processing semantics by default", which read as though UI could never be
included in a full-screen effect. That is wrong, and post-processing over UI is
a legitimate thing to want.

Post-processing in this engine attaches to a render target, not to a camera:
bloom-system dedupes with processedTargetsThisFrame.has(renderTarget), so an
effect is applied once per target regardless of how many cameras drew into it.
That single fact gives all three scopes with no new plumbing - world-only via
separate targets (the default), everything via pointing the UI camera at the
world camera's target, and UI-only via attaching effects to the UI camera.
Ordering already works for the shared case, since post-processing systems
register after the render system and before the present system.

Also records the one real constraint: a shared target means one set of effects
over both, so bloom-the-world-but-not-the-UI combined with tone-map-everything
needs two passes. Notes that as the argument for eventually attaching a
post-processing stack to the present step rather than only to a camera, and
marks it out of scope here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): fix stale field name in DL-13 after the DL-14 rename

DL-13's consequences still listed wasClickedThisFrame, which DL-14 renamed to
wasActivatedThisFrame, and omitted isFocused entirely. Also notes that
isFocused and wasActivatedThisFrame are written by the navigation system as
well as the interaction system, so neither has a single owning system.

Found by a consistency sweep over the document rather than by CI, which has not
run on this branch since 031d13b.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): state whether DL-03 survives the transform fix, and correct the argument

Answers directly whether the canvas-scaling decision changes once #581 lands.
It does not, but the original rationale leaned on "(b) is broken" as its
headline argument, which was the one objection with an expiry date.

Adds a per-channel table making the composition rules explicit, since they
differ and only one is wrong: rotation composes additively (correct - nesting
rotations adds angles), scale composes multiplicatively (correct - a 2x parent
with a 3x child is 6x), and position composes additively but omits
transforming the local offset by the parent's scale and rotation first. So
#581 is not "make scale and rotation additive like position"; those are already
right.

Records that once #581 lands, option (b) stops being broken - a scaled canvas
root would scale children's offsets, and sprite sizes already scale via
bindSpriteInstanceData - but still loses on the load-bearing arguments: it
dirties every UI entity's world transform on resize where (a) changes one
camera field, it puts rects in a screen-dependent space instead of reference
pixels, it invalidates the isStatic freeze from DL-12 on every resize, and it
makes a per-element press animation scale against a screen-dependent base.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): record how the two-camera setup constrains DL-07's pointer

The camera separation described in section 3 does affect DL-07, and the
decision did not say so. Because UI renders through its own camera, one
canvas-space pointer maps to two different world positions - one through each
camera - and they diverge as soon as the world camera pans or zooms.

So PointerEcsComponent publishes canvas pixels only and stays camera-agnostic
as well as source-agnostic. It must not publish a world position, because there
is no single correct one. Conversion is the caller's job and screenToWorldSpace
is already camera-parameterized, so the UI raycaster converts through the UI
camera and world picking converts through the world camera, with neither
privileged.

Also records the consistency requirement in the other direction: the raycaster
must only hit-test what the UI camera can see, since an element culled by
cullingMask that remains clickable produces invisible hit regions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): reconcile text input staying in UI while text rendering moves out

Answers why a text input belongs in this module when text rendering does not.
They look inconsistent but aren't: a text input is three separable concerns.
Text entry (keystrokes, caret, IME, clipboard, soft keyboard) is an input
concern and almost always implies a focused widget, unlike damage numbers or
world signage. Text display is identical to drawing any other text and belongs
to #584. The focusable rect it hangs off is exactly what this module is.

Fixes a real gap in the backlog bookkeeping while there: item 3.4 was marked
blocked on #583 but 3.5 was not marked blocked on #584, despite being far more
blocked. A scroll view merely looks wrong without clipping; a text field is
unusable without text rendering, since you cannot see what you type.

Also splits the hidden-input bridge out into /src/input, for the same reason
DL-07 moved the pointer there. That bridge is where the IME and mobile-keyboard
complexity lives and is reusable by anything needing text entry rather than
being specific to one widget. As a result the "no DOM-backed widgets" non-goal
becomes a real invariant instead of one with an exception carved out of it -
the UI module now stays DOM-free.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): split text input out into its own issue (#586)

Text input follows text rendering and clipping out of the UI module. It is the
strongest candidate of the three: it was the most hard-blocked item in the
backlog (unusable, not merely imperfect, without #584), and its genuinely hard
part - IME composition, mobile soft keyboards, clipboard, selection,
accessibility - is an input concern rather than a UI one.

#586 covers a text-entry primitive in /src/input wrapping a hidden DOM input,
plus the thin UI component that consumes it. DL-10 stays as the decision record
with a scope banner, and item 3.5 is marked out of scope.

Also flags something for #584 before its API is fixed: rendering a caret and
selection highlight needs per-glyph x-positions, so TextMeshEcsComponent should
expose glyph advances rather than only quads. Retrofitting that later is more
disruptive than including it from the start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): replace Rect in place rather than adding a parallel Rect2

Per review, the plain-object rect keeps the name Rect and replaces the existing
class outright, rather than coexisting with it under a new name. Not documented
as a breaking change.

The blast radius turns out to be genuinely small, which supports replacing
rather than deprecating: Rect is constructed nowhere in /src outside its own
tests, and its only consumer is CameraEcsComponent.scissorRect, whose type name
does not change. What changes is construction and the two methods becoming
static calls.

Mirrors vector2.ts exactly - a plain interface for the data, a static-only class
for the operations with mutating helpers taking a target first argument. Keeps
{ min, max } over the current { origin, size }, since anchor math reads min/max
far more often and size is one subtraction away.

Folds in a file rename to rect.ts, since src/math/Rect.ts violates the
kebab-case convention in AGENTS.md and a full rewrite is the cheapest moment to
fix it.

Leaves one naming question open in section 11: Vector2/Vec2 puts the long name
on the type and the short one on the namespace, which does not translate since
Rect has no natural abbreviation. Rects is proposed; Rectangle/Rect is the more
literal mirror.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

* docs(ui): update for the transform fix and AGENTS.md correction landing on dev

Merges dev, which brings in #587 (the parent-transform fix filed as #581) and
#588 (the AGENTS.md input capability correction filed as #582). Both were
surfaced by the review of this document, and both are now fixed, so the
document's claims about them were stale.

The transform gap row and its explanatory note move to past tense. #587 landed
exactly the composition the note proposed - scale the child's local offset by
the parent's world scale, rotate it by the parent's world rotation, then add -
and deleted the three superseded parent-*-system.ts files along with it. The
note is retained rather than removed, since the design's assumptions were formed
against the broken behavior and it is worth knowing they no longer are.

DL-03's "does this survive the fix" subsection moves to past tense too, and now
records that option (b) is no longer broken while still losing on the
load-bearing arguments.

Open question 2 loses its blocker: world-space canvas mode depended on the
transform fix, since a health bar parented to a rotating ship was exactly the
broken case.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WmVKWzRGU5FPVEwAa2oqVw

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(agents): AGENTS.md overstates input source capabilities (claims touch support that doesn't exist)

2 participants